home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Software Vault: The Gold Collection
/
Software Vault - The Gold Collection (American Databankers) (1993).ISO
/
cdr52
/
udfs.zip
/
UDFS.TXT
< prev
Wrap
Text File
|
1993-04-02
|
34KB
|
747 lines
THE ABC's OF UDF's
By Greg Lief
I. INTRODUCTION AND BACKGROUND
A. Clipper vs. dBASE
Do you remember the first time you saw or heard about Clipper?
Chances are that your first impression of it was that of a
dBASE compiler. That feature alone makes Clipper worthwhile.
However, as you take the time to delve into Clipper, you
quickly realize that there is more to Clipper than just a
"dBASE compiler". The Clipper language addresses nearly all
of the shortcomings of the dBASE language by providing numerous
additional functions and commands. But more important than all
those new goodies is Clipper's open architecture, which allows
you to write and/or link in routines written in "C", Assembler,
and (surprise!) Clipper. Such Clipper routines are known as
User-Defined Functions.
User-Defined Functions (UDFs) open up an entirely new avenue of
options that are simply inconceivable in a dBASE environment.
For those of you that have not taken the time to write your own
UDFs already, think of them as your own personal language
extensions. Plus, you do not have to learn another (often
cryptic) language to write them.
B. Benefits of UDFs
A few basic benefits of UDFs include:
- getting the absolute most out of the wonderfully open-ended
Clipper language;
- minimizing repetitious coding in your applications, thus
reducing overhead requirements, maintenance time/cost/
frustration, and increasing efficiency;
- allowing you to develop your own libraries of UDFs rather
than having to reinvent the wheel with each new occurrence
of the same old problem, thereby increasing productivity;
- achieving consistency among your applications by using the
same functions "across the board", which decreases confusion
(both for your clients and you);
A side benefit to UDFs is that they gently but firmly force you
to adopt better structured programming techniques. For a
language (dBASE, and hence Clipper) that can be very
unstructured, we need all the guidance we can get! UDFs
encourage modular programming, rather than throwing everything
into one procedure and hoping you will never have to debug it.
C. A Few Examples
Some of the myriad purposes for User-Defined Functions include:
1. Data validation
Without UDFs, you are limited to useful (but simple) VALID
clauses, or RANGE checking.
With UDFs, you can perform complex look-ups into related
databases or arrays, and display verbose descriptions found
therein. You can also provide the user meaningful error
messages in the event that their data does not pass muster,
rather than having them sit there wondering why "the cursor
won't move".
2. Common Occurrences, such as yes/no prompts, error messages,
or displaying file directories for file-related operations
Without UDFs, you are faced with redundant code, which leads
to difficult maintenance.
With UDFs, you have the code in just one place. This is a
blessing when you decide that those green on magenta yes/no
prompts are not as pleasant as you originally thought. Now
you can change it once instead of ten (or more) times.
3. Centering character strings on the screen or printer
Without UDFs, you have to type in the cumbersome
"INT((80-LEN(string))/2)" formula throughout your program.
Don't forget the last parenthesis, or the compiler will
scream at you!
With UDFs, you waste no time retyping said formula, which
results in cleaner code and a clearer head.
4. Cosmetic enhancements
Without UDFs, you have the same old humdrum database
management look and feel (or should we call it "yawn and
stretch"?), which some people like and most people are
resigned to.
With UDFs, you can have exploding / pull-down / pop-up
boxes, falling character strings, splitting screens, and all
kinds of other crazy things that will get your users'
attention IMMEDIATELY.
5. "Hot-key" procedures, including pop-up help screens and
many other utilities
Without UDFs, such utilities are impossible and unthinkable.
With UDFs, the sky (and your imagination) is the limit!
II. UDF BASICS
A. Structure
Consider the following skeleton of a (recently deceased) UDF:
FUNCTION funcname
PARAMETERS param1, param2, ...
PRIVATE return_value, etc...
|
| code to manipulate data
|
RETURN (return_value)
i. Name that Function, and Grab Those Parameters!
First, we name the UDF using the FUNCTION (or PROCEDURE)
statement. Next, the PARAMETERS statement is used to
receive any parameters that will be sent to this UDF from
the calling program. Note that some UDFs will not require
the use of parameters, in which case the PARAMETERS
statement may be cheerfully omitted.
ii. PRIVATE Variables
The next step is to declare PRIVATE all variables that will
be "local" to this UDF. Why bother with this? There are
two reasons. First, all variables available, or "visible",
to the calling program will also be available to the UDF.
Therefore, any changes or assignments made to variables
within the UDF will affect such variables in the context of
main program as well UNLESS YOU DECLARE THEM PRIVATE TO THE
UDF.
This point is quite important, and is the source of many
subtle bugs if not heeded. Suppose that you have defined a
variable named OLDSCRN in your main program that holds a
crucial saved screen. Now further suppose that you wish
your UDF to display a message or a scrolling window,
thereby affecting the screen. You would probably want to
save that portion of the screen to a buffer so that the UDF
could then restore it properly upon exit. Finally, suppose
that you save that window to the variable OLDSCRN within
the UDF without declaring it as PRIVATE to the UDF. No
local copy of the variable will be made, which means that
you will overwrite the previous value of OLDSCRN, which
will almost certainly wreak havoc once you return to the
calling program.
Another secondary reason for declaring variables PRIVATE
within a UDF is so that you can keep track of what
variables you are using in your application and where they
are being used.
iii. The Meat of The UDF
This consists of the code that will manipulate the data, or
cure cancer, or whatever it is that this UDF is intended to
do.
iv. A Speedy RETURN
We wrap up the UDF with a RETURN statement. With
functions, this statement MUST return a value back to the
calling program (even if this value will be ignored).
It is strongly advised that you follow the practice of
having only one RETURN statement in your functions, rather
than something like the following:
FUNCTION myudf
PARAMETER mNAME, mID
IF mNAME = 'GRUMPFISH'
RETURN (.T.)
ELSEIF mNAME = 'HAPPYFISH"
RETURN (.F.)
ELSEIF mID = '99999'
RETURN (.T.)
ENDIF
As you can see from this substandard piece of code, there
are no less than three exit points from this UDF. Although
this is a fairly simple example, multiple exit points can
can make debugging very difficult when you begin working
with UDFs of greater complexity. The following is a
drastic improvement:
FUNCTION myudf
PARAMETER mNAME, mID
PRIVATE ret_val
ret_val = .F. && guilty until proven innocent
IF mNAME = 'GRUMPFISH' .OR. mID = '99999'
ret_val = .T.
ENDIF
RETURN (ret_val)
B. FUNCTION or PROCEDURE?
The term "User-Defined Function" refers not only to functions,
but to procedures as well. The fundamental difference between
these two beasts is that functions return a value, whereas
procedures do not. However, Clipper allows you to begin a
program statement with a function [such as the ever-popular
INKEY(0)], in which case the return value is ignored. You may
also have functions that always return the same value, with the
express intent that you will ignore that value.
Other differences exist as well, particularly relating to
parameters, which we will explore momentarily...
III. PARAMETERS
A. Introduction
There are some functions that always return one value, such as
TIME() and DATE(). These functions have a predefined mission
that cannot be altered by sending them additional information.
However, when you write your own UDFs, you will most likely want
to exert more control over them. For example, if you design a
UDF that pops up an error box, you may initially write it to use
a generic message such as "Error - press any key to continue".
Chances are that you (and your users) will quickly tire of this
uninformative error message. In this instance, whenever you call
the UDF you would want to pass it a message to be displayed.
You could then easily construct the UDF to act upon the passed
message to draw a box wide enough to accommodate it. This
message is known as a PARAMETER.
B. Formal vs. Actual
Parameters are memory variables that receive values or
references passed to a function or procedure. Parameters are
known as either "formal" or "actual". Formal parameters are
the receiving memory variables specified as the arguments of
the PARAMETERS statement. Actual parameters are the arguments
of the call to the procedure (DO..WITH) or UDF [MyUdf(...)].
Note that the number of formal and actual parameters do not
have to match.
Let us use the following example to explore the various
ramifications of having more or less actual parameters than
formal parameters. Here is the UDF we will use:
FUNCTION MyFunc
PARAMETERS mvar1, mvar2, mvar3, mvar4
PRIVATE ret_val
ret_val = ((mvar1 * 2.75) + (mvar2 * mvar3)) / (mvar4 + 5)
RETURN (ret_val)
As you can see, MyFunc() is an obtuse number-cruncher. It
accepts four parameters, performs a strange calculation
involving them, and returns the value of said calculation.
What happens when we call MyFunc() with:
1) "result = MyFunc(10, 5, 7, 20)"
The number of actual and formal parameters match exactly.
MVAR1 assumes the value 10, MVAR2 assumes the value 5,
MVAR3 assumes the value 7, and MVAR4 assumes the value of
20. For those of you keeping score, the return value is
2.5, which will be stored in RESULT.
2) "result = MyFunc(10, 5, 7, 20, 76, 120)"
Here we are passing six actual parameters. Since there
are only four parameters in the formal list, the function
will only act upon the first four actual parameters. As
in example 1), MVAR1 assumes the value 10, MVAR2 assumes
the value 5, MVAR3 assumes the value 7, and MVAR4 assumes
the value of 20. The parameters 76 and 120 are discarded.
3) "result = MyFunc(10, 5, 7)"
In this example we pass only three actual parameters. With
MyFunc(), this is asking for trouble because the UDF
explicitly acts upon all four parameters. You can see
that, as above, MVAR1 assumes the value 10, MVAR2 assumes
the value 5, and MVAR3 assumes the value 7. However, MVAR4
will be undefined since we have not passed a fourth
parameter to match it. This will cause MyFunc() to crash
when it reaches the calculation statement with the message
"unidentified identifier MVAR4".
C. Number and Type Checking
That little crash in Example 3 could have been avoided using
the Clipper function PCOUNT(), which checks the number of
actual parameters passed. To make our little number-cruncher a
bit more bulletproof, we could rewrite it as follows:
FUNCTION MyFunc
PARAMETERS mvar1, mvar2, mvar3, mvar4
PRIVATE ret_val
ret_val = 0
** did they pass all four parameters?
IF PCOUNT() > 3
ret_val = ((mvar1 * 2.75) + (mvar2 * mvar3)) / (mvar4 + 5)
ELSE
** guess not - time for sound and fury
? 'Error in call to MyFunc()'
TONE(220,1)
TONE(220,1)
INKEY(0)
ENDIF
RETURN (ret_val)
However, suppose that we are coding late one night, and we are
very tired. So tired, in fact, that we make a careless mistake
like the following:
STORE 10 TO val1, val2, val3, val4
|
val3 = 'Could be trouble'
|
result = MyFunc(val1, val2, val3, val4)
When we finally get to MyFunc(), the variable MVAR3 will assume
the value of VAL3, which has inadvertently been defined as a
character string. This will cause a "Type Mismatch" explosion
when MyFunc() attempts to perform a numeric operation on a
string. However, we can avoid this as well by using the Clipper
function TYPE(). Obviously enough, TYPE() checks the type of a
variable that is passed to it in the form of a character
string. For example, in this instance "TYPE('val3')" would
return a value of "C" for character, because VAL3 is a
character-type variable.
With the TYPE() function under our belts, let us take one more
stab at MyFunc():
FUNCTION MyFunc
PARAMETERS mvar1, mvar2, mvar3, mvar4
PRIVATE ret_val, err_msg
ret_val = 0
** did they pass all four parameters?
IF PCOUNT() > 3
** are all parameters numeric type?
IF TYPE('mvar1') = 'N' .AND. TYPE('mvar2') = 'N' .AND. ;
TYPE('mvar3') = 'N' .AND. TYPE('mvar4') = 'N'
ret_val = ((mvar1*2.75) + (mvar2*mvar3)) / (mvar4+5)
ELSE
err_msg = 'Type mismatch in parameters to MyFunc()'
ENDIF
ELSE
err_msg = 'Not enough parameters passed to MyFunc()'
ENDIF
** see if error message was defined - if so, display it
IF TYPE('err_msg) = 'C'
? err_msg
TONE(220,1)
TONE(220,1)
INKEY(0)
ENDIF
RETURN (ret_val)
As you can see, the MyFunc() code continues to grow, but it is
now virtually bullet-proof. In this incarnation, you can call
from it anywhere in your program without having to worry about
the program crashing because of incorrect parameters. This is
good programming practice for you to follow when creating your
own UDFs for several reasons:
1) Some other programmer may eventually use your UDF,
especially if you are working in a team environment, and
you should make every attempt to shield them from fatal
(i.e., immediate exit to DOS) errors.
2) Nobody is perfect, and it is entirely possible that at some
future time you too could accidentally call a UDF with the
wrong parameters, so why not shield yourself as well?
Also note the use of meaningful error messages based on the
type of error. This is not exactly earth-shattering but may
save someone valuable time when debugging.
D. Reference vs. Value
When a parameter is passed by VALUE, the UDF evaluates it and
makes a "local" copy of the resultant value at a different
memory address. Whenever the UDF needs to work with that
parameter, it refers to the new memory address rather than
using the memory address of the original parameter (which is
unknown to it). This is ideal when you want the UDF to
manipulate or tear asunder some variable without affecting its
value once control returns to the calling program.
"And in this corner..." passing parameters by REFERENCE means
that instead of passing the value of a variable, you instead
are passing a pointer to a memory address that actually
contains the variable. Unlike parameters passed by value, no
"local" copy is made; thus if you change that parameter within
the UDF, you are effectively changing the actual value of that
variable.
Memory variables are passed by reference to PROCEDURES, and by
value to FUNCTIONS. Consider the following example:
* Main.prg
mvar = 50
DO MultByFive WITH mvar
? mvar
RETURN
*
PROCEDURE MultByFive
PARAMETERS testing
testing = testing * 5
RETURN
Since variables are passed by reference to PROCEDURES,
MultByFive is actually changing the value of the variable MVAR.
When we return to Main.prg, the value of MVAR will be 250,
rather than 50. However, the equivalent FUNCTION would allow
you to manipulate this variable while protecting it in the
calling program:
* Main.prg
mvar = 50
? MultByFive(mvar)
? mvar
RETURN
*
FUNCTION MultByFive
PARAMETERS testing
testing = testing * 5
RETURN (testing)
Because parameters are passed by value to FUNCTIONS, MultByFive
evaluates MVAR and places that value in a different memory
address, which it then refers to as TESTING. When it
multiplies TESTING by 5, it is only changing the "local" copy
of the variable rather than manipulating MVAR. Thus, when
control returns to Main.prg, the value of MVAR will still be
50.
There are instances where you may wish to override these basic
defaults. For example, one good reason to pass variables by
reference to FUNCTIONS is speed. It takes time for the
FUNCTION to create the local copy of the variable. Passing by
reference, however, eliminates this need, and can increase
throughput by an enormous factor. To pass a variable by
reference to a function, simply precede it with the "AT" sign
(@). For instance, in the last example we could have used the
statement "? MultByFive(@mvar)". Bear in mind that this would
have caused MultByFive() to change the value of MVAR to 250.
In similar fashion, you can pass variables to PROCEDURES by
value. This is useful if you do not want the procedure to
inadvertently change the value of a variable in the calling
program. Had we chosen this route in our PROCEDURE-al example
above, the syntax would have been "DO MultByFive WITH (mvar)".
Of course, we would probably have modified MultByFive to
display the calculated value, or else it would be an exercise
in futility.
Database fields are always passed by value to FUNCTIONS and
PROCEDURES, and must be bounded by parentheses. Array names
are always passed by reference. This is sensible, because it
would be incredibly time-consuming to pass a large array to a
UDF, only to have the UDF then make a copy of it for local
manipulation. Array elements and expressions are always
passed by value.
IV. HOUSEKEEPING
If you are designing a UDF that in any way changes the
environment (color, screen, coordinates, work area, cursor
status), it is good practice to save all items that will
be changed upon entry of the UDF, and restore them upon
exit. The following code fragment illustrates this:
FUNCTION look_up
PRIVATE oldscrn, oldcolor, work_area, oldrow, oldcol, oldcurs
** save environment
SAVE SCREEN TO oldscrn
oldcolor = SETCOLOR()
work_area = SELECT()
oldrow = ROW()
oldcol = COL()
oldcurs = IsCursor()
|
| code to manipulate data
|
** restore environment
RESTORE SCREEN FROM oldscrn
SETCOLOR(oldcolor)
SELECT(work_area)
SET CURSOR (oldcurs)
@ oldrow, oldcol SAY ''
RETURN(return_value)
This particular UDF is going to be used to look up values in a
related database, and will change the entire environment
(screen, color, work area, etc.). Therefore, we must save the
current settings so that we can restore them properly upon
exit.
First, we declare our local variables as PRIVATE to avoid
potential conflicts with other variables of the same names.
Next, we use the Clipper command SAVE SCREEN to save the
screen, because we intend to change it. SETCOLOR() returns the
current color setting, which we must save because we might
change the color. SELECT() returns the current work area.
ROW() and COL() return the current screen row and column
coordinates, respectively. IsCursor() is a public domain
routine written by John Scott Prinke, which returns the current
state of the cursor as a logical value (.T. means on).
V. HOT-KEY PROCEDURES
A. Activating the Hot Key
A "hot-key" procedure is one that is activated by a designated
keypress. To create and use a hot-key procedure, you must
first define the hot-key in your main program in this manner:
* Main.prg
EXTERNAL <hotkey UDF> && not always necessary
SET KEY <nn> TO <hotkey UDF> && assign hot key to the UDF
A complete listing of scan codes for all of the function key,
Ctrl-key, and Alt-key combinations can be found in your Clipper
manual.
B. External
The EXTERNAL command must be used if you intend to link in a
routine from a library rather than from an object module. The
reason for EXTERNAL stems from the modus operandi of the
Clipper compiler. When you call a procedure with "DO
procname", the compiler creates a symbol named PROCNAME. If
this symbol remains unresolved before link-time, (i.e., the
compiler does not find it in any of the compiled .prg files),
the linker will first attempt to resolve it by searching the
other object files that you are linking in. If it is
unsuccessful, it will then search through the specified
libraries. However, the compiler does not create a symbol when
you use the SET KEY command. For example, you wish to link in
the Grumpfish Library appointment tracker and set the F10 key
to activate it. You forget about EXTERNAL, and only use:
SET KEY -9 TO popdate
This will look fine through compilation and linking, but as
soon as you get into the program and begin pressing F10, you
will get the nasty run-time error "MISSING EXTERNAL POPDATE".
As Spock would readily point out, this is perfectly logical,
because you forgot to instruct the linker to link in this
module.
C. Basic Structure
The structure of a hot-key procedure is similar to that of a
UDF, with several key differences (pun intentional):
PROCEDURE <procname>
PARAMETERS proc, line, var && built-in Clipper parameters!
SET KEY <nn> TO && to prevent recursion
|
| code to manipulate data
|
SET KEY <nn> TO <hotkey UDF> && reset for later use
RETURN
1. Built-in Clipper PARAMETERS
The PARAMETERS line is advisable, because whenever you
execute a SET KEY procedure, Clipper automatically passes
three parameters to the procedure:
- Procedure Name (always upper-case), Source Code
- Line Number (0 if the source code was compiled with
the "line numbers off" switch)
- Variable Name being READ (always upper-case)
You do not necessarily have to trap these parameters, but in
many cases they may be useful to know. One instance where
you would definitely want this information is if you are
building a context-specific help procedure, such as the
Grumpfish Help System. You may wish to note, however, that
since line numbers are quite volatile, you should not rely
too heavily upon them in a help system. As a matter of
fact, the Grumpfish Help System completely ignores line
numbers, acting instead only upon the procedure and variable
names.
2. Recursion and How to Avoid It
In the line after the PARAMETERS line, we use SET KEY <.>
TO. This prevents recursion - otherwise, the user could
continue to press the hot-key from within the hot-key
procedure, which would then call itself repeatedly until
both you and it would be thoroughly confused. The last line
before the RETURN statement resets the hot key.
If you are using a number of hot keys, you may wish to
consider creating UDFs that will turn all of the hot keys on
and off. You could then call those UDFs to prevent the user
from "hot-keying" willy-nilly through the program:
PROCEDURE <procname>
PARAMETERS proc, line, var && built-in Clipper parameters!
HotKeysOff()
|
| code to manipulate data
|
HotKeysOn()
RETURN
D. Housekeeping
Nearly all hot-key procedures will change the environment in
some manner, which makes the aforementioned good housekeeping
techniques even more crucial. Get in the habit of saving
volatile items like the current color and affected portions of
the screen right at the top of your hot-key procedures.
VI. MAKING YOUR OWN UDF LIBRARY
So you have built up an impressive collection of fully debugged
UDFs. Great! But aren't you getting a bit tired of typing all
those object (.OBJ) modules on the link line, and aren't your
directories getting a bit cluttered? The best way to kill both of
these birds with one stone is to consolidate all of your UDFs into
a library (.LIB) file with the Microsoft Library Manager (TM).
A. Using the Library Manager
The Microsoft Library Manager is pre-packaged with a number of
other Microsoft products, including their C compiler and some
versions of DOS. (Look for the file LIB.EXE in your DOS
directory or on your DOS supplemental disk.) Using LIB to
create and modify your own function libraries is simple. The
syntax for LIB is:
LIB <Libname> <Commands>, <Listfile>, <Output>
<Libname> is the name of the library (the .lib extension is not
necessary).
<Commands> are of the general format <symbol>filename, and must
be separated by spaces. The available symbols include:
+ add modulename to the library
- remove modulename from the library
* extract modulename without removing from library
-+ replace modulename in library
-* extract modulename and remove from library
<Listfile> is the name of the list file to be generated, which
lists the module names in the library and the memory usage for
each module.
<Output> is the name of the output library file. This is useful
if you wish to make a new library that essentially duplicates
another with some modifications.
If you do not wish to generate a list file or a new output
library file, you may follow the command list with a semi-
colon.
Note that you may also run LIB without command line parameters,
in which instance you will be prompted for each item.
B. Preparing your source code
The most efficient way to build a library is to compile each of
your UDFs separately, excepting instances where you know that
certain UDFs always must be linked in together. The reason for
isolating each UDF is because when you call the library with
your linker, the linker will pull in only what you have asked
for in your source code.
Let us suppose that you have called the pop-up calculator from
Grumpfish Library with the source code line "DO POPCALC". When
you compile this source code, the compiler creates the symbol
POPCALC, which the linker will then attempt to resolve by
searching the libraries for an object module of that name.
However, because I compiled each of my UDFs separately before
creating Grumpfish Library, the linker will only link in
POPCALC, rather than pulling in the entire library.
By contrast, imagine that you have ten short UDFs in one .prg
file. You compile this file and put it into a library. If you
call any one of those ten UDFs, the linker will be forced to
link in ALL of them because they are all part of the same object
module. This is additional overhead that you simply do not
need. Therefore, you should take the time to compile each UDF
separately.
C. Examples
Let us suppose that you have four object files: LOOKUP.obj,
REC_LOCK.obj, REC_SRCH.obj, and CENTER.obj. You wish to
combine these into one .lib file named MYFUNCS.lib. You do
not need a list file, nor do you need a different name for
the output library. Here is the command you would use:
LIB myfuncs +lookup +rec_lock +rec_srch +center ;
This will create MYFUNCS.LIB, which will contain the four
object files. Then instead of linking in each .obj file like
so:
PLINK86 FI myprog, dup_chk, rec_lock, rec_srch, center ;
LI \clipper\extend,\clipper\clipper
you merely link in the library:
PLINK86 FI mfile LI myfuncs,\clipper\extend,\clipper\clipper
Suppose that later you make changes to the source code of
REC_LOCK.prg and need to recompile it. You would then update
MYFUNCS.LIB with the following command:
LIB myfuncs -+rec_lock;
This removes the module REC_LOCK from the library, then adds in
the newer version. Naturally, you must have the file
REC_LOCK.OBJ in the same directory, or else you must add the
path specifier so that LIB can find it.
If you decided later that you wanted to extract the object code
for REC_LOCK, you would use this command:
LIB myfuncs *rec_lock;
This would extract the file REC_LOCK.OBJ without removing it
from the MYFUNCS library.